(function () {

	const _changeEvent = {
		type: 'change'
	};
	const _startEvent = {
		type: 'start'
	};
	const _endEvent = {
		type: 'end'
	};

	class TrackballControls extends THREE.EventDispatcher {

		constructor(object, domElement) {

			super();
			if (domElement === undefined) console.warn('THREE.TrackballControls: The second parameter "domElement" is now mandatory.');
			if (domElement === document) console.error('THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.');
			const scope = this;
			const STATE = {
				NONE: - 1,
				ROTATE: 0,
				ZOOM: 1,
				PAN: 2,
				TOUCH_ROTATE: 3,
				TOUCH_ZOOM_PAN: 4
			};
			this.object = object;
			this.domElement = domElement;
			this.domElement.style.touchAction = 'none'; // disable touch scroll
			// API

			this.enabled = true;
			this.screen = {
				left: 0,
				top: 0,
				width: 0,
				height: 0
			};
			this.rotateSpeed = 1.0;
			this.zoomSpeed = 1.2;
			this.panSpeed = 0.3;
			this.noRotate = false;
			this.noZoom = false;
			this.noPan = false;
			this.staticMoving = false;
			this.dynamicDampingFactor = 0.2;
			this.minDistance = 0;
			this.maxDistance = Infinity;
			this.keys = ['KeyA',
				/*A*/
				'KeyS',
				/*S*/
				'KeyD'
				/*D*/
			];
			this.mouseButtons = {
				LEFT: THREE.MOUSE.ROTATE,
				MIDDLE: THREE.MOUSE.DOLLY,
				RIGHT: THREE.MOUSE.PAN
			}; // internals

			this.target = new THREE.Vector3();
			const EPS = 0.000001;
			const lastPosition = new THREE.Vector3();
			let lastZoom = 1;
			let _state = STATE.NONE,
				_keyState = STATE.NONE,
				_touchZoomDistanceStart = 0,
				_touchZoomDistanceEnd = 0,
				_lastAngle = 0;

			const _eye = new THREE.Vector3(),
				_movePrev = new THREE.Vector2(),
				_moveCurr = new THREE.Vector2(),
				_lastAxis = new THREE.Vector3(),
				_zoomStart = new THREE.Vector2(),
				_zoomEnd = new THREE.Vector2(),
				_panStart = new THREE.Vector2(),
				_panEnd = new THREE.Vector2(),
				_pointers = [],
				_pointerPositions = {}; // for reset


			this.target0 = this.target.clone();
			this.position0 = this.object.position.clone();
			this.up0 = this.object.up.clone();
			this.zoom0 = this.object.zoom; // methods

			this.handleResize = function () {

				const box = scope.domElement.getBoundingClientRect(); // adjustments come from similar code in the jquery offset() function

				const d = scope.domElement.ownerDocument.documentElement;
				scope.screen.left = box.left + window.pageXOffset - d.clientLeft;
				scope.screen.top = box.top + window.pageYOffset - d.clientTop;
				scope.screen.width = box.width;
				scope.screen.height = box.height;

			};

			const getMouseOnScreen = function () {

				const vector = new THREE.Vector2();
				return function getMouseOnScreen(pageX, pageY) {

					vector.set((pageX - scope.screen.left) / scope.screen.width, (pageY - scope.screen.top) / scope.screen.height);
					return vector;

				};

			}();

			const getMouseOnCircle = function () {

				const vector = new THREE.Vector2();
				return function getMouseOnCircle(pageX, pageY) {

					vector.set((pageX - scope.screen.width * 0.5 - scope.screen.left) / (scope.screen.width * 0.5), (scope.screen.height + 2 * (scope.screen.top - pageY)) / scope.screen.width // screen.width intentional
					);
					return vector;

				};

			}();

			this.rotateCamera = function () {

				const axis = new THREE.Vector3(),
					quaternion = new THREE.Quaternion(),
					eyeDirection = new THREE.Vector3(),
					objectUpDirection = new THREE.Vector3(),
					objectSidewaysDirection = new THREE.Vector3(),
					moveDirection = new THREE.Vector3();
				return function rotateCamera() {

					moveDirection.set(_moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0);
					let angle = moveDirection.length();

					if (angle) {

						_eye.copy(scope.object.position).sub(scope.target);

						eyeDirection.copy(_eye).normalize();
						objectUpDirection.copy(scope.object.up).normalize();
						objectSidewaysDirection.crossVectors(objectUpDirection, eyeDirection).normalize();
						objectUpDirection.setLength(_moveCurr.y - _movePrev.y);
						objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x);
						moveDirection.copy(objectUpDirection.add(objectSidewaysDirection));
						axis.crossVectors(moveDirection, _eye).normalize();
						angle *= scope.rotateSpeed;
						quaternion.setFromAxisAngle(axis, angle);

						_eye.applyQuaternion(quaternion);

						scope.object.up.applyQuaternion(quaternion);

						_lastAxis.copy(axis);

						_lastAngle = angle;

					} else if (!scope.staticMoving && _lastAngle) {

						_lastAngle *= Math.sqrt(1.0 - scope.dynamicDampingFactor);

						_eye.copy(scope.object.position).sub(scope.target);

						quaternion.setFromAxisAngle(_lastAxis, _lastAngle);

						_eye.applyQuaternion(quaternion);

						scope.object.up.applyQuaternion(quaternion);

					}

					_movePrev.copy(_moveCurr);

				};

			}();

			this.zoomCamera = function () {

				let factor;

				if (_state === STATE.TOUCH_ZOOM_PAN) {

					factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
					_touchZoomDistanceStart = _touchZoomDistanceEnd;

					if (scope.object.isPerspectiveCamera) {

						_eye.multiplyScalar(factor);

					} else if (scope.object.isOrthographicCamera) {

						scope.object.zoom *= factor;
						scope.object.updateProjectionMatrix();

					} else {

						console.warn('THREE.TrackballControls: Unsupported camera type');

					}

				} else {

					factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * scope.zoomSpeed;

					if (factor !== 1.0 && factor > 0.0) {

						if (scope.object.isPerspectiveCamera) {

							_eye.multiplyScalar(factor);

						} else if (scope.object.isOrthographicCamera) {

							scope.object.zoom /= factor;
							scope.object.updateProjectionMatrix();

						} else {

							console.warn('THREE.TrackballControls: Unsupported camera type');

						}

					}

					if (scope.staticMoving) {

						_zoomStart.copy(_zoomEnd);

					} else {

						_zoomStart.y += (_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor;

					}

				}

			};

			this.panCamera = function () {

				const mouseChange = new THREE.Vector2(),
					objectUp = new THREE.Vector3(),
					pan = new THREE.Vector3();
				return function panCamera() {

					mouseChange.copy(_panEnd).sub(_panStart);

					if (mouseChange.lengthSq()) {

						if (scope.object.isOrthographicCamera) {

							const scale_x = (scope.object.right - scope.object.left) / scope.object.zoom / scope.domElement.clientWidth;
							const scale_y = (scope.object.top - scope.object.bottom) / scope.object.zoom / scope.domElement.clientWidth;
							mouseChange.x *= scale_x;
							mouseChange.y *= scale_y;

						}

						mouseChange.multiplyScalar(_eye.length() * scope.panSpeed);
						pan.copy(_eye).cross(scope.object.up).setLength(mouseChange.x);
						pan.add(objectUp.copy(scope.object.up).setLength(mouseChange.y));
						scope.object.position.add(pan);
						scope.target.add(pan);

						if (scope.staticMoving) {

							_panStart.copy(_panEnd);

						} else {

							_panStart.add(mouseChange.subVectors(_panEnd, _panStart).multiplyScalar(scope.dynamicDampingFactor));

						}

					}

				};

			}();

			this.checkDistances = function () {

				if (!scope.noZoom || !scope.noPan) {

					if (_eye.lengthSq() > scope.maxDistance * scope.maxDistance) {

						scope.object.position.addVectors(scope.target, _eye.setLength(scope.maxDistance));

						_zoomStart.copy(_zoomEnd);

					}

					if (_eye.lengthSq() < scope.minDistance * scope.minDistance) {

						scope.object.position.addVectors(scope.target, _eye.setLength(scope.minDistance));

						_zoomStart.copy(_zoomEnd);

					}

				}

			};

			this.update = function () {

				_eye.subVectors(scope.object.position, scope.target);

				if (!scope.noRotate) {

					scope.rotateCamera();

				}

				if (!scope.noZoom) {

					scope.zoomCamera();

				}

				if (!scope.noPan) {

					scope.panCamera();

				}

				scope.object.position.addVectors(scope.target, _eye);

				if (scope.object.isPerspectiveCamera) {

					scope.checkDistances();
					scope.object.lookAt(scope.target);

					if (lastPosition.distanceToSquared(scope.object.position) > EPS) {

						scope.dispatchEvent(_changeEvent);
						lastPosition.copy(scope.object.position);

					}

				} else if (scope.object.isOrthographicCamera) {

					scope.object.lookAt(scope.target);

					if (lastPosition.distanceToSquared(scope.object.position) > EPS || lastZoom !== scope.object.zoom) {

						scope.dispatchEvent(_changeEvent);
						lastPosition.copy(scope.object.position);
						lastZoom = scope.object.zoom;

					}

				} else {

					console.warn('THREE.TrackballControls: Unsupported camera type');

				}

			};

			this.reset = function () {

				_state = STATE.NONE;
				_keyState = STATE.NONE;
				scope.target.copy(scope.target0);
				scope.object.position.copy(scope.position0);
				scope.object.up.copy(scope.up0);
				scope.object.zoom = scope.zoom0;
				scope.object.updateProjectionMatrix();

				_eye.subVectors(scope.object.position, scope.target);

				scope.object.lookAt(scope.target);
				scope.dispatchEvent(_changeEvent);
				lastPosition.copy(scope.object.position);
				lastZoom = scope.object.zoom;

			}; // listeners


			function onPointerDown(event) {

				if (scope.enabled === false) return;

				if (_pointers.length === 0) {

					scope.domElement.setPointerCapture(event.pointerId);
					scope.domElement.addEventListener('pointermove', onPointerMove);
					scope.domElement.addEventListener('pointerup', onPointerUp);

				} //


				addPointer(event);

				if (event.pointerType === 'touch') {

					onTouchStart(event);

				} else {

					onMouseDown(event);

				}

			}

			function onPointerMove(event) {

				if (scope.enabled === false) return;

				if (event.pointerType === 'touch') {

					onTouchMove(event);

				} else {

					onMouseMove(event);

				}

			}

			function onPointerUp(event) {

				if (scope.enabled === false) return;

				if (event.pointerType === 'touch') {

					onTouchEnd(event);

				} else {

					onMouseUp();

				} //


				removePointer(event);

				if (_pointers.length === 0) {

					scope.domElement.releasePointerCapture(event.pointerId);
					scope.domElement.removeEventListener('pointermove', onPointerMove);
					scope.domElement.removeEventListener('pointerup', onPointerUp);

				}

			}

			function onPointerCancel(event) {

				removePointer(event);

			}

			function keydown(event) {

				if (scope.enabled === false) return;
				window.removeEventListener('keydown', keydown);

				if (_keyState !== STATE.NONE) {

					return;

				} else if (event.code === scope.keys[STATE.ROTATE] && !scope.noRotate) {

					_keyState = STATE.ROTATE;

				} else if (event.code === scope.keys[STATE.ZOOM] && !scope.noZoom) {

					_keyState = STATE.ZOOM;

				} else if (event.code === scope.keys[STATE.PAN] && !scope.noPan) {

					_keyState = STATE.PAN;

				}

			}

			function keyup() {

				if (scope.enabled === false) return;
				_keyState = STATE.NONE;
				window.addEventListener('keydown', keydown);

			}

			function onMouseDown(event) {

				if (_state === STATE.NONE) {

					switch (event.button) {

						case scope.mouseButtons.LEFT:
							_state = STATE.ROTATE;
							break;

						case scope.mouseButtons.MIDDLE:
							_state = STATE.ZOOM;
							break;

						case scope.mouseButtons.RIGHT:
							_state = STATE.PAN;
							break;

						default:
							_state = STATE.NONE;

					}

				}

				const state = _keyState !== STATE.NONE ? _keyState : _state;

				if (state === STATE.ROTATE && !scope.noRotate) {

					_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));

					_movePrev.copy(_moveCurr);

				} else if (state === STATE.ZOOM && !scope.noZoom) {

					_zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY));

					_zoomEnd.copy(_zoomStart);

				} else if (state === STATE.PAN && !scope.noPan) {

					_panStart.copy(getMouseOnScreen(event.pageX, event.pageY));

					_panEnd.copy(_panStart);

				}

				scope.dispatchEvent(_startEvent);

			}

			function onMouseMove(event) {

				const state = _keyState !== STATE.NONE ? _keyState : _state;

				if (state === STATE.ROTATE && !scope.noRotate) {

					_movePrev.copy(_moveCurr);

					_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));

				} else if (state === STATE.ZOOM && !scope.noZoom) {

					_zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY));

				} else if (state === STATE.PAN && !scope.noPan) {

					_panEnd.copy(getMouseOnScreen(event.pageX, event.pageY));

				}

			}

			function onMouseUp() {

				_state = STATE.NONE;
				scope.dispatchEvent(_endEvent);

			}

			function onMouseWheel(event) {

				if (scope.enabled === false) return;
				if (scope.noZoom === true) return;
				event.preventDefault();

				switch (event.deltaMode) {

					case 2:
						// Zoom in pages
						_zoomStart.y -= event.deltaY * 0.025;
						break;

					case 1:
						// Zoom in lines
						_zoomStart.y -= event.deltaY * 0.01;
						break;

					default:
						// undefined, 0, assume pixels
						_zoomStart.y -= event.deltaY * 0.00025;
						break;

				}

				scope.dispatchEvent(_startEvent);
				scope.dispatchEvent(_endEvent);

			}

			function onTouchStart(event) {

				trackPointer(event);

				switch (_pointers.length) {

					case 1:
						_state = STATE.TOUCH_ROTATE;

						_moveCurr.copy(getMouseOnCircle(_pointers[0].pageX, _pointers[0].pageY));

						_movePrev.copy(_moveCurr);

						break;

					default:
						// 2 or more
						_state = STATE.TOUCH_ZOOM_PAN;
						const dx = _pointers[0].pageX - _pointers[1].pageX;
						const dy = _pointers[0].pageY - _pointers[1].pageY;
						_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy);
						const x = (_pointers[0].pageX + _pointers[1].pageX) / 2;
						const y = (_pointers[0].pageY + _pointers[1].pageY) / 2;

						_panStart.copy(getMouseOnScreen(x, y));

						_panEnd.copy(_panStart);

						break;

				}

				scope.dispatchEvent(_startEvent);

			}

			function onTouchMove(event) {

				trackPointer(event);

				switch (_pointers.length) {

					case 1:
						_movePrev.copy(_moveCurr);

						_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));

						break;

					default:
						// 2 or more
						const position = getSecondPointerPosition(event);
						const dx = event.pageX - position.x;
						const dy = event.pageY - position.y;
						_touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy);
						const x = (event.pageX + position.x) / 2;
						const y = (event.pageY + position.y) / 2;

						_panEnd.copy(getMouseOnScreen(x, y));

						break;

				}

			}

			function onTouchEnd(event) {

				switch (_pointers.length) {

					case 0:
						_state = STATE.NONE;
						break;

					case 1:
						_state = STATE.TOUCH_ROTATE;

						_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));

						_movePrev.copy(_moveCurr);

						break;

					case 2:
						_state = STATE.TOUCH_ZOOM_PAN;

						_moveCurr.copy(getMouseOnCircle(event.pageX - _movePrev.pageX, event.pageY - _movePrev.pageY));

						_movePrev.copy(_moveCurr);

						break;

				}

				scope.dispatchEvent(_endEvent);

			}

			function contextmenu(event) {

				if (scope.enabled === false) return;
				event.preventDefault();

			}

			function addPointer(event) {

				_pointers.push(event);

			}

			function removePointer(event) {

				delete _pointerPositions[event.pointerId];

				for (let i = 0; i < _pointers.length; i++) {

					if (_pointers[i].pointerId == event.pointerId) {

						_pointers.splice(i, 1);

						return;

					}

				}

			}

			function trackPointer(event) {

				let position = _pointerPositions[event.pointerId];

				if (position === undefined) {

					position = new THREE.Vector2();
					_pointerPositions[event.pointerId] = position;

				}

				position.set(event.pageX, event.pageY);

			}

			function getSecondPointerPosition(event) {

				const pointer = event.pointerId === _pointers[0].pointerId ? _pointers[1] : _pointers[0];
				return _pointerPositions[pointer.pointerId];

			}

			this.dispose = function () {

				scope.domElement.removeEventListener('contextmenu', contextmenu);
				scope.domElement.removeEventListener('pointerdown', onPointerDown);
				scope.domElement.removeEventListener('pointercancel', onPointerCancel);
				scope.domElement.removeEventListener('wheel', onMouseWheel);
				scope.domElement.removeEventListener('pointermove', onPointerMove);
				scope.domElement.removeEventListener('pointerup', onPointerUp);
				window.removeEventListener('keydown', keydown);
				window.removeEventListener('keyup', keyup);

			};

			this.domElement.addEventListener('contextmenu', contextmenu);
			this.domElement.addEventListener('pointerdown', onPointerDown);
			this.domElement.addEventListener('pointercancel', onPointerCancel);
			this.domElement.addEventListener('wheel', onMouseWheel, {
				passive: false
			});
			window.addEventListener('keydown', keydown);
			window.addEventListener('keyup', keyup);
			this.handleResize(); // force an update at start

			this.update();

		}

	}

	THREE.TrackballControls = TrackballControls;

})();
